/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. See accompanying LICENSE file.
 */
package com.hortonworks.registries.auth.client;

import com.hortonworks.registries.auth.server.AuthenticationFilter;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;

/**
 * The {@link AuthenticatedURL} class enables the use of the JDK {@link URL} class
 * against HTTP endpoints protected with the {@link AuthenticationFilter}.
 * <p>
 * The authentication mechanisms supported by default are Simple  authentication
 * (also known as pseudo authentication) and Kerberos SPNEGO authentication.
 * <p>
 * Additional authentication mechanisms can be supported via {@link Authenticator} implementations.
 * <p>
 * The default {@link Authenticator} is the {@link KerberosAuthenticator} class which supports
 * automatic fallback from Kerberos SPNEGO to Simple authentication.
 * <p>
 * <code>AuthenticatedURL</code> instances are not thread-safe.
 * <p>
 * The usage pattern of the {@link AuthenticatedURL} is:
 * <pre>
 *
 * // establishing an initial connection
 *
 * URL url = new URL("http://foo:8080/bar");
 * AuthenticatedURL.Token token = new AuthenticatedURL.Token();
 * AuthenticatedURL aUrl = new AuthenticatedURL();
 * HttpURLConnection conn = new AuthenticatedURL(url, token).openConnection();
 * ....
 * // use the 'conn' instance
 * ....
 *
 * // establishing a follow up connection using a token from the previous connection
 *
 * HttpURLConnection conn = new AuthenticatedURL(url, token).openConnection();
 * ....
 * // use the 'conn' instance
 * ....
 *
 * </pre>
 */
public class AuthenticatedURL {

    /**
     * Name of the HTTP cookie used for the authentication token between the client and the server.
     */
    public static final String AUTH_COOKIE = "hadoop.auth";

    private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";

    /**
     * Client side authentication token.
     */
    public static class Token {

        private String token;

        /**
         * Creates a token.
         */
        public Token() {
        }

        /**
         * Creates a token using an existing string representation of the token.
         *
         * @param tokenStr string representation of the tokenStr.
         */
        public Token(String tokenStr) {
            if (tokenStr == null) {
                throw new IllegalArgumentException("tokenStr cannot be null");
            }
            set(tokenStr);
        }

        /**
         * Returns if a token from the server has been set.
         *
         * @return if a token from the server has been set.
         */
        public boolean isSet() {
            return token != null;
        }

        /**
         * Sets a token.
         *
         * @param tokenStr string representation of the tokenStr.
         */
        void set(String tokenStr) {
            token = tokenStr;
        }

        /**
         * Returns the string representation of the token.
         *
         * @return the string representation of the token.
         */
        @Override
        public String toString() {
            return token;
        }

    }

    private static Class<? extends Authenticator> DEFAULT_AUTHENTICATOR = KerberosAuthenticator.class;

    /**
     * Sets the default {@link Authenticator} class to use when an {@link AuthenticatedURL} instance
     * is created without specifying an authenticator.
     *
     * @param authenticator the authenticator class to use as default.
     */
    public static void setDefaultAuthenticator(Class<? extends Authenticator> authenticator) {
        DEFAULT_AUTHENTICATOR = authenticator;
    }

    /**
     * Returns the default {@link Authenticator} class to use when an {@link AuthenticatedURL} instance
     * is created without specifying an authenticator.
     *
     * @return the authenticator class to use as default.
     */
    public static Class<? extends Authenticator> getDefaultAuthenticator() {
        return DEFAULT_AUTHENTICATOR;
    }

    private Authenticator authenticator;
    private ConnectionConfigurator connConfigurator;

    /**
     * Creates an {@link AuthenticatedURL}.
     */
    public AuthenticatedURL() {
        this(null);
    }

    /**
     * Creates an <code>AuthenticatedURL</code>.
     *
     * @param authenticator the {@link Authenticator} instance to use, if <code>null</code> a {@link
     *                      KerberosAuthenticator} is used.
     */
    public AuthenticatedURL(Authenticator authenticator) {
        this(authenticator, null);
    }

    /**
     * Creates an <code>AuthenticatedURL</code>.
     *
     * @param authenticator    the {@link Authenticator} instance to use, if <code>null</code> a {@link
     *                         KerberosAuthenticator} is used.
     * @param connConfigurator a connection configurator.
     */
    public AuthenticatedURL(Authenticator authenticator,
                            ConnectionConfigurator connConfigurator) {
        try {
            this.authenticator = (authenticator != null) ? authenticator : DEFAULT_AUTHENTICATOR.newInstance();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        this.connConfigurator = connConfigurator;
        this.authenticator.setConnectionConfigurator(connConfigurator);
    }

    /**
     * Returns the {@link Authenticator} instance used by the
     * <code>AuthenticatedURL</code>.
     *
     * @return the {@link Authenticator} instance
     */
    protected Authenticator getAuthenticator() {
        return authenticator;
    }

    /**
     * Returns an authenticated {@link HttpURLConnection}.
     *
     * @param url   the URL to connect to. Only HTTP/S URLs are supported.
     * @param token the authentication token being used for the user.
     * @return an authenticated {@link HttpURLConnection}.
     * @throws IOException             if an IO error occurred.
     * @throws AuthenticationException if an authentication exception occurred.
     */
    public HttpURLConnection openConnection(URL url, Token token) throws IOException, AuthenticationException {
        if (url == null) {
            throw new IllegalArgumentException("url cannot be NULL");
        }
        if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
            throw new IllegalArgumentException("url must be for a HTTP or HTTPS resource");
        }
        if (token == null) {
            throw new IllegalArgumentException("token cannot be NULL");
        }
        authenticator.authenticate(url, token);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        if (connConfigurator != null) {
            conn = connConfigurator.configure(conn);
        }
        injectToken(conn, token);
        return conn;
    }

    /**
     * Helper method that injects an authentication token to send with a connection.
     *
     * @param conn  connection to inject the authentication token into.
     * @param token authentication token to inject.
     */
    public static void injectToken(HttpURLConnection conn, Token token) {
        String t = token.token;
        if (t != null) {
            if (!t.startsWith("\"")) {
                t = "\"" + t + "\"";
            }
            conn.addRequestProperty("Cookie", AUTH_COOKIE_EQ + t);
        }
    }

    /**
     * Helper method that extracts an authentication token received from a connection.
     * <p>
     * This method is used by {@link Authenticator} implementations.
     *
     * @param conn  connection to extract the authentication token from.
     * @param token the authentication token.
     * @throws IOException             if an IO error occurred.
     * @throws AuthenticationException if an authentication exception occurred.
     */
    public static void extractToken(HttpURLConnection conn, Token token) throws IOException, AuthenticationException {
        int respCode = conn.getResponseCode();
        if (respCode == HttpURLConnection.HTTP_OK
                || respCode == HttpURLConnection.HTTP_CREATED
                || respCode == HttpURLConnection.HTTP_ACCEPTED) {
            Map<String, List<String>> headers = conn.getHeaderFields();
            List<String> cookies = headers.get("Set-Cookie");
            if (cookies != null) {
                for (String cookie : cookies) {
                    if (cookie.startsWith(AUTH_COOKIE_EQ)) {
                        String value = cookie.substring(AUTH_COOKIE_EQ.length());
                        int separator = value.indexOf(";");
                        if (separator > -1) {
                            value = value.substring(0, separator);
                        }
                        if (value.length() > 0) {
                            token.set(value);
                        }
                    }
                }
            }
        } else {
            token.set(null);
            throw new AuthenticationException("Authentication failed, status: " + conn.getResponseCode() +
                    ", message: " + conn.getResponseMessage());
        }
    }

}
